Letztes Update: 04.07.2015
Behandelte Befehle: throw, throws, try, catch, Exception, RuntimeException, printStackTrace()

In diesem Kapitel sehen wir uns an, wie man in Java mit Fehlern umgeht. Einen Fehler nennt man beim Programmieren eine Exception (engl. Ausnahme).

25.1 Compiler-Fehler

Zunächst mal rufen wir uns ins Gedächtnis zurück, dass unser Java-Code übersetzt wird in ein "maschinenfreundliches" Format, den sogenannten Bytecode. Diese Übersetzung übernimmt der Compiler. Der Compiler findet bereits viele Fehler, die sogenannten Compiler-Fehler. Solange diese Fehler nicht beseitigt sind, können Sie Ihr Programm gar nicht starten. Daher geht es in diesem Kapitel um die anderen, die sogenannten Laufzeit-Fehler, die dann auftreten, wenn das Programm läuft.

Damit wir zunächst mal wissen, was mit Compiler-Fehlern gemeint ist: Beispiele für Fehler, die der Compiler abfängt, sind Syntaxfehler, d.h. Fehler, die aufgrund von falscher Schreibung oder "Grammatik" auftreten. Beispiele sind falsch geschriebene Kommandos (wile statt while), Beispiele für falsche Grammatik sind vergessene Semicolons oder nicht geschlossene Klammern.

Der Compiler kümmert sich aber auch um semantische Fehler, d.h. Code, der zwar syntaktisch stimmt, aber ein inhaltliches Problem hat. Beispiel dafür ist die Typ-Kontrolle:

boolean foo = false;
int num = 10 + foo; // Exception: kann kein Bool addieren

Weiteres Beispiel ist die Überprüfung, ob eine Funktion mit Rückgabetyp auch wirklich immer ein return aufruft:

public int minusAberPositiv(int a, int b) {
if (a > b) {
return a + b;
}
// Exception: was passiert bei a <= b ?
}

25.2 Geprüfte Exceptions

Wie schon angedeutet, wollen wir Fehler behandeln, die während der Programmausführung, also zur Laufzeit auftreten. Nehmen wir folgendes Beispiel:

public class Konto {
private double guthaben = 0;

public void abheben(double betrag) {
guthaben -= betrag;
}

public void einzahlen(double betrag) {
guthaben += betrag;
}

public String toString() {
return "Kontostand: " + guthaben;
}
}

Ein Beispielprogramm wäre:

public static void main(String[] a) {
Konto k = new Konto();
k.einzahlen(100);
k.abheben(20);
System.out.println(k);
}
Kontostand: 80.0

Nichts hindert uns jedoch daran, zu Beginn einen negativen Betrag "einzuzahlen":

public static void main(String[] a) {
Konto k = new Konto();
k.einzahlen(-100);
k.abheben(20);
System.out.println(k);
}
Kontostand: -120.0

Fehler werfen

Um das zu beheben, müssen wir unsere Methode einzahlen mit eine Mechanismus ausstatten, der eine Exception "wirft", falls ein negativer Betrag eingezahlt wird.

public void einzahlen(double betrag) {
if (betrag < 0) {
throw new Exception(
"Keinen negativen Betrag einzahlen: "
+ betrag);
}
guthaben += betrag;
}

Es fehlte noch ein Signal, dass innerhalb dieser Methode eine Exception geworfen wird:

public void einzahlen(double betrag) throws Exception {
if (betrag < 0) {
throw new Exception(
"Keinen negativen Betrag einzahlen: "
+ betrag);
}
guthaben += betrag;
}

Wozu diese "Markierung" in der ersten Zeile der Methode? Ganz einfach: Jeder, der diese Methode aufruft, weiß jetzt, dass die Methode einen möglichen Fehler auslöst und muss sich entscheiden, wie damit umzugehen ist. Die erste Möglichkeit ist, den Fehler zu "fangen".

Fehler fangen mit try-catch

Nehmen wir an, wir hätten eine Methode, die eine Reihe von Einzahlungen vornimmt. Diese Methode muss die Methode einzahlen aufrufen:

public void vieleEinzahlungen(double[] betraege) {
for (int i = 0; i < betraege.length; i++) {
einzahlen(betraege[i]);
}
}

Der Compiler erlaubt dies nicht. Sie müssen den Fall, dass bei der Einzahlung etwas schiefläuft abfangen mit folgendem Konstrukt:

try {
CODE
} catch (Exception e) {
FEHLERFALL-CODE
}

Der "normale" Code wird von einem try eingeklammert. Sobald eine Exception auftritt, wird dieser Code-Block verlassen und es wird in den Fehlerfall-Code gesprungen. Tritt kein Fehler auf, wird der Fehlerfall-Code ignoriert.

In unserem Beispiel wäre das:

public void vieleEinzahlungen(double[] betraege) {
try {
for (int i = 0; i < betraege.length; i++) {
einzahlen(betraege[i]);
}
} catch (Exception e) {
System.out.println("Fehler! " + e.getMessage());
}
}

Testen wir jetzt unseren Code mit:

public static void main(String[] a) {
Konto k = new Konto();
double[] testnum = { 10, -5, 200 };
k.vieleEinzahlungen(testnum);
System.out.println(k);
}

Dann sehen wir, dass der Code bei der zweiten Zahl abbricht bzw. in den Fehlercode-Block läuft, wo eine Ausgabe ("Fehler! ...") stattfindet. Anschließend springt ist die Methode fertig und der Kontostand wird ausgegeben. Wir sehen, dass nur die erste Einzahlung (= 10) geklappt hat:

Fehler! Keinen negativen Betrag einzahlen: -5.0
Kontostand: 10.0

Allgemein sieht die Ausführungsfolge beim Fangen einer Exception wie folgt aus (boo sei eine Methode, die eine BooException wirft):

Wie Sie sehen, wird der Code im try-Block unterhalb der Zeile, in der der Fehler auftritt, nicht mehr ausgeführt. Es wird in den catch-Block gesprungen und dannach ganz normal weiter Code abgearbeitet.

Ein großer Bereich für geprüfte Exceptions sind Ein-/Ausgabe-Operationen zu externen Speichermedien oder über das Netzwerk. Nehmen wir an, Sie möchten eine Datei mit der Klasse Scanner einlesen. Sobald Sie den Konstruktor aufrufen, kann eine FileNotFoundException geworfen werden, die Sie abfangen müssen:

try {
File file = new File("data.txt");
Scanner scanner = new Scanner(file);
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
System.out.println("> " + line);
}
scanner.close();
} catch (FileNotFoundException ex) {
ex.printStackTrace();
}

Sie können das in der Java-API sehen:

Werfen und Fangen

Sie fragen sich vielleicht gerade, was das ganze soll mit dem Fangen und Werfen? Bei der Fehlerbehandlung sieht man sich mit zwei Problemen konfrontiert:

  1. Wie kommuniziert man, dass ein Fehler vorliegt (und welcher Art dieser ist)?
  2. Wo behandelt man diesen Fehler?

Für die erste Frage haben wir offensichtlich jetzt eine Lösung. Wohingegen man "früher" (bevor Exceptions erfunden wurden) einfach einen "ungewöhnlichen" Wert zurückgegeben hat, um einen Fehler zu signalisieren, kann man jetzt, mit Hilfe des Exception-Objekts, eine klare Botschaft senden. Ein Beispiel für eine "alte" Lösung:

// Gibt true zurück, falls Einzahlung erfolgreich, sonst false.
public boolean einzahlen(double betrag) {
if (betrag < 0) {
return false;
}
guthaben += betrag;
return true;
}

Die zweite Frage ist schwieriger: Wo wird der Fehler behandelt? Stellen Sie sich vor, Sie haben drei Methodenaufrufe:

kundenAktion() ruft auf:
vieleEinzahlungen(double[] betraege) ruft auf:
einzahlen(double betrag)

Die Methode "kundenAktion()" gehört zu einer Reihe von GUI-Aktionen. In welcher Methode würden Sie einen Fehler, der in "einzahlen()" auftritt, behandeln? Die Antwort ist: in der GUI-Methode! Denn dort können Sie z.B. ein Fenster mit einer Fehlermeldung zeigen oder einen Dialog zur Fehlerkorrektur anstoßen.

Aber wie schaffen Sie es, dass erst die Methode kundenAktion() den Fehler behandelt? Ganz einfach: Sie werfen den Fehler immer weiter. Dazu müssen Sie lediglich ein throws in der Methodendefinition hinzufügen. Im Fehlerfall springt Java aus der Methode raus und geht an den Ort zurück, wo die letzte Methode aufgerufen wurde.

Schauen wir uns anhand unserer Konto-Klasse an, wie ein Fehler über mehrere Methoden weitergeworfen wird.

Auf der untersten Ebene wird der Fehler geworfen (Kommando throw) und die Methode wird markiert (Schlüsselwort throws):

public void einzahlen(double betrag) throws Exception {
if (betrag < 0) {
throw new Exception(
"Keinen negativen Betrag einzahlen: "
+ betrag);
}
guthaben += betrag;
}

Auf der nächsten Ebene wird der Fehler nicht behandelt, sondern die Methode wird einfach markiert, um zu sagen: "Hier kann auch ein Fehler auftreten!":

public void vieleEinzahlungen(double[] betraege) throws
																		 Exception
{
for (int i = 0; i < betraege.length; i++) {
einzahlen(betraege[i]);
}
}

Erst bei der Methode, die den Fehler wirklich sinnvoll behandeln kann, wird mit try-catch der Fehler abgefangen. Dann ist natürlich keine Markierung mit throws mehr angebracht, da der Fehler nicht weitergeleitet wird.

public void kundenAktion()
{
...
try {
...
vieleEinzahlungen(b);
...
} catch (Exception e) {
... // z.B. Fenster mit Klärung
}
}

Eigene geprüfte Exception schreiben

Exceptions sind Objekte, die z.B. eine Nachricht enthalten (siehe Code oben). In Java gibt es eine Hierarchie von Fehlerklassen, an deren Spitze die Klasse Throwable (engl. werfbar) steht.

Uns interessiert hauptsächlich die Java-Klasse Exception. Sie ist die Oberklasse von geprüften Exceptions abgesehen von den Klassen unter RuntimeException (siehe nächsten Abschnitt). Die Unterklassen von Exception drücken mit Ihrem Namen bereits das behandelte Problemen aus, z.B. FileNotFoundException.

Wenn Sie ein größeres Projekt schreiben, definieren Sie sich Ihre eigenen Fehlerklassen, indem Sie eine Unterklasse von Exception erstellen. Für das Beispiel oben zum Beispiel die Klasse NegativeEinzahlungException.

public class NegativeEinzahlungException extends Exception {

public NegativeEinzahlungException() {
}

public NegativeEinzahlungException(String message) {
super(message);
}

}

Die Definition leitet nur die Information an die Oberklasse Exception weiter. Ihr Beitrag ist lediglich der Name der Klasse. Jetzt können Sie die Klasse in Ihrem Projekt einsetzen:

public void einzahlen(double betrag) throws NegativeEinzahlungException {
if (betrag < 0) {
throw new NegativeEinzahlungException(
"Keinen negativen Betrag einzahlen: "
+ betrag);
}
guthaben += betrag;
}

Beim Fangen des Fehlers wird der Name der Exception berücksichtigt:

public void vieleEinzahlungen(double[] betraege) {
try {
for (int i = 0; i < betraege.length; i++) {
einzahlen(betraege[i]);
}
} catch (NegativeEinzahlungException e) {
System.out.println("Fehler! " + e.getMessage());
}
}

Exception-Hierarchie beim Fangen

Wenn Sie eine Methode aufrufen, die eine FileNotFoundException wirft, können Sie beim Fangen auch eine Oberklasse verwenden. Das ist nützlich, wenn Sie mehrere Methoden in try-Block haben, die Exceptions werfen.

Nehmen wir an, Sie rufen die Methode someFileMethod auf, die eine FileNotFoundException wirft. Dann können Sie die Exception wie folgt abfangen:

try {
someFileMethod("bla.txt");
} catch (FileNotFoundException e) {
System.out.println("Fehler: " + e);
}

Sie können aber auch die Oberklasse IOException fangen:

try {
someFileMethod("bla.txt");
} catch (IOException e) {
System.out.println("Fehler: " + e);
}

Das hätte hier die gleichen Konsequenzen. Als Daumenregel sagt man, man möge die spezifischste Exception fangen, in diesem Fall also FileNotFoundException.

Wenn Sie zwei Methoden aufrufen, die beiden Exceptions werfen, können Sie aber eine allgemeinere Exception fangen (die Klasse Exception z.B. geht immer):

try {
vieleEinzahlungen(fooArray);
someFileMethod("bla.txt");
} catch (Exception e) {
System.out.println("Fehler: " + e);
}

Alternativ können Sie auch beide Exceptions separat abfangen, indem Sie einen weiteren catch-Block anhängen.

try {
vieleEinzahlungen(fooArray);
someFileMethod("bla.txt");
} catch (IOException e) {
System.out.println("Schreib-/Lesefehler: " + e);
} catch (NegativeEinzahlungException e) {
System.out.println("Problem mit der Einzahlung: " + e);
}

Übungsaufgaben

(a) Eigene Exception

Schreiben Sie eine eigene Exception namens MyException wie oben beschrieben.

(b) Exception behandeln

Wir setzen voraus, dass Sie die Klasse MyException geschrieben haben. Im gleichen Projekt schreiben Sie die nächste Klasse. Diese Klasse enthält drei Methoden:

public class ExceptionDemo {

public void topLevel() {
midLevel();
}

public void midLevel() {
lowLevel(0);
}

public void lowLevel(int x) throws MyException {
if (x == 0) {
throw new MyException("Du Null!");
}
System.out.println("Alles gut.");
}

public static void main(String[] args) {
ExceptionDemo d = new ExceptionDemo();
d.topLevel();
}
}

Die low-level Methode wirft eine Exception. Diese Exception soll aber erst in topLevel() gefangen werden. Ergänzen Sie den Code entsprechend.

25.3 Ungeprüfte Exceptions

Die im vorigen Abschnitt beschriebene Methode der Fehlerverarbeitung ist zwar sehr "sauber", führt aber zu einer Menge Code. Für eine Reihe von Fällen wäre es "Overkill", diesen Mechanismus zu verwenden.

Wann können Fehler auftreten. Zum Beispiel, wenn man durch Null teilen will (a und b seien Variablen vom Typ double):

double x = a / b;
Oder wenn Sie auf einen Array zugreifen, könnte der Index außerhalb der Grenzen des Arrays sein (namen sei ein Array von Strings, index eine Integer-Variable):
String foo = namen[index];

Oder wenn Sie eine Methode aufrufen, könnte die Variable statt eines Objekts null enthalten (foo sei eine Variable vom Typ String):

int size = foo.length();

In alle Fällen werden von Java Exceptions geworfen, aber das erstaunliche ist: Sie müssen kein try-catch anwenden. Der Grund: diese Exceptions sind ungeprüfte Exceptions und ungeprüfte Exceptions müssen nicht gefangen werden.

Rein technisch sind die ungeprüfte Exceptions Unterklassen der Klasse RuntimeException. Eine "Runtime-Exception" ist also eine ungeprüfte Exception, wobei der Name eine klare Fehlbenennung ist, denn alle hier diskutierten Fehler, geprüft oder ungeprüft, sind "Laufzeit-Fehler". Die andere Sorte Fehler, Compiler-Fehler, findet hier ja keine Beachtung, sie müssen schließlich nicht im Code berücksichtigt werden.

Wichtig: Da ungeprüfte Exceptions in der Regel nicht gefangen werden, wird im Fehlerfall das Programm beendet. Im Gegensatz dazu läuft bei einer geprüften Exception, die entsprechend gefangen werden muss, das Programm normalerweise weiter.

Ungeprüfte Exception schreiben

Wenn Sie eine eigene Exception lieber ungeprüft lassen wollen, dann erstellen Sie eine Unterklasse der Klasse RuntimeException.

Im obigen Beispiel würde man das so definieren:

public class NegativeEinzahlungException extends RuntimeException {

public NegativeEinzahlungException() {
}

public NegativeEinzahlungException(String message) {
super(message);
}

}

Der Unterschied zu vorher: Alle Methoden, die eine solche Exception werfen würden, müssen nicht markiert werden, selbst wenn sie eine NegativeEinzahlungException werfen:

public void einzahlen(double betrag) {
if (betrag < 0) {
throw new NegativeEinzahlungException(
"Keinen negativen Betrag einzahlen: "
+ betrag);
}
guthaben += betrag;
}

Und natürlich müssen Methoden, die solche Methoden mit Fehlerpotential aufrufen, nicht mit try-catch der Status der Methode testen

public void vieleEinzahlungen(double[] betraege) {
for (int i = 0; i < betraege.length; i++) {
einzahlen(betraege[i]);
}
}

Bestehende Exceptions nutzen

Wenn es bereits eine Exception-Klasse gibt, die von der Bedeutung her auf Ihren Fehlerfall passt, dann sollten Sie einfach die bestehende Klasse benutzen.

Sehen Sie sich dazu in der Java-API die Unterklassen von RuntimeException an:

Nehmen wir an, Sie haben eine Klasse Party, wo Sie alle Teilnehmer in einer Liste gespeichert haben. Sie erlauben das Hinzufügen von neuen Teilnehmern mit der Methode meldeAn:

public class Party {

private ArrayList<String> teilnehmer = new ArrayList<>();

public void meldeAn(String name) {
teilnehmer.add(name);
}
}

Jetzt möchten Sie vermeiden, dass unsinniger Input als Anmeldung durchgeht, z.B. ein leerer String "" oder auch ein String nur aus Leerzeichen " ". Sie möchten eine Exception werfen, allerdings soll es sich um eine RuntimeException handeln, damit Ihre Methode nicht zu unhandlich wird.

In der Java-API finden Sie die Exception IllegalArgumentException. Diese Exception ist genau für solche Fälle geschaffen, dass Sie ungeeignete Eingabewerte abfangen. Da es sich bereits um eine ungeprüfte Exception handelt, können Sie sie einfach verwenden, ohne eine eigene Klasse zu schreiben:

public class Party {

private ArrayList<String> teilnehmer = new ArrayList<>();

public void meldeAn(String name) {
if (name.trim().length() == 0) {
throw new IllegalArgumentException("Anmeldung mit leerem Namen.");
}
teilnehmer.add(name);
}
}

Die Methode trim() entfernt alle Leerzeichen zu Beginn und Ende eines Strings (z.B. aus " hallo welt   " wird "hallo welt"). Da IllegalArgumentException eine Unterklasse von RuntimeException ist, muss die Exception weder behandelt noch weitergeworfen werden.

25.4 Wichtige Anwendungsregeln

Nie den catch-Block leer lassen

In der Java-Bibliothek gibt es viele Methoden, die Exceptions werfen. Ein Programmieranfänger mag versucht sein, den entsprechenden Methodenaufruf einfach mit einem try-catch zu umgeben und den catch-Block leer zu lassen:

...
try {
doSomething();
} catch (SomeException e) {
}

Das führt natürlich dazu, dass Sie im Fehlerfall nicht davon erfahren, dass ein Fehler an genau dieser Stelle auftrat, so dass die Fehlersuche sehr aufwändig werden kann.

Die Minimallösung für solche Standardsituationen ist:

...
try {
doSomething();
} catch (SomeException e) {
e.printStackTrace();
}

Mit der Methode printStackTrace() wird ausgegeben, welche Methoden bis zum Aufkommen der Exception aufgerufen wurden, inklusive der Zeile und Meldung des aktuellen Fehlers.

Wann geprüft, wann ungeprüft?

Wenn Sie eigene Fehlerklassen schreiben, müssen Sie entscheiden, ob Sie eine geprüfte oder ungeprüfte Exception verwenden.

Eine Daumenregel dazu besagt:

  • Geprüfte Exceptions verwenden, wenn ein Fehler auf äußere Umstände zurückgeht, z.B. dass eine Datei nicht am richtigen Ort ist, die Netzwerkleitung nicht funktioniert oder der Benutzer eine falsche Eingabe gemacht hat. Diese Fehler kann der Programmierer nie beseitigen, also lohnt es sich, im Code entsprechende Abfangmechanismen zu implementieren, so dass das Programm im Fehlerfall weiterlaufen kann.
  • Ungeprüfte Exception verwenden, wenn ein Programmierfehler vorliegt, z.B. ein Zugriff auf nicht vorhandene Elemente eines Arrays (oder einer Liste), der Aufruf einer Funktion mit illegalen Parameterwerten oder falsches Casten von Typen. Diese Fehler sollten nach und nach aus dem Programm entfernt werden, daher ist es gut, wenn die Exceptions nicht aufgefangen werden, sondern das Programm stoppt.

Übungsaufgabe

(a) Todo-Liste

Schreiben Sie Backend und GUI für die Verwaltung einer Todo-Liste. Jeder Todo-Eintrag ist ein "Task" und hat einen Titel und eine Priorität zwischen 1 und 3:

(In der GUI wird mit Absicht kein Slider für die Priorität benutzt.)

Ihr Backend besteht aus den zwei Klassen Task mit den Eigenschaften title (String) und priority (int) und TodoList mit der Eigenschaft tasks (Liste von Task-Objekten). Die Klassen sollen in einem Package mit Namen de.hsa.todo.model sitzen. Die Klasse TodoList hat die Methoden addTask(String, int) und printAll().

Die GUI wird in einer JavaFX-Klasse TodoListUI realisiert, in einem separaten Package mit Namen de.hsa.todo.view. Bei "Add" wird ein neuer Task hinzugefügt, bei "Print" werden alle Tasks auf der Konsole ausgegeben, bei "Close" schließt das Fenster.

Anmerkung: Die Trennung von Backend und GUI in zwei Packages folgt der Praxis des Model-View-Controller-Konzepts, das wir im nächsten Kapitel noch kennen lernen. Die Backend-Klassen nennt man auch ein Modell, die GUI-Klasse vereint sowohl View als auch Controller.

Hinweis: Mit Integer.parseInt() können Sie einen String in eine ganze Zahl umwandeln. Für den Fall, dass es sich bei dem String nicht um eine Zahl handelt, wird eine NumberFormatException geworfen (ist diese geprüft oder ungeprüft? Schauen Sie in die API!).

Wenn der Benutzer auf "Add" drückt, können drei "Fehler" passieren:

  1. Der Titel ist leer
  2. Die Priorität ist keine Zahl
  3. Die Priorität ist eine Zahl aber nicht zwischen 1 und 3

Sie sollen diese Fehler mit Exception-Handling abfangen. Überlegen Sie sich, wo die Exception geworfen wird (sollte nicht im UI-Code passieren). Überlegen Sie auch, ob Sie eine eigene Exception schreiben oder ob Sie eine vorhandene nehmen.

Geben Sie im einfachsten Fall die Fehlermeldungen auf der Konsole aus:

Titel ist leer.
Priorität muss Zahl sein.
Priorität muss 1, 2 oder 3 sein.

Wenn Sie möchten, können Sie eine Meldungsleiste hinzufügen, die nur eingeblendet wird (FadeIn und FadeOut), wenn eine Meldung kommt, z.B. nachdem ein Task hinzugefügt wurde:

Fügen Sie dazu ein Label ein und fügen Sie es dem zentralen Panel hinzu (unter Umständen innerhalb einer HBox, damit Sie das Label zentrieren können). Setzen Sie die Opaqueheit auf 0. Erzeugen Sie dann zwei FadeTransitions, die Sie mit SequentialTransition kombinieren (siehe voriges Kapitel). Um eine Meldung abzufeuern, setzen Sie zunächst den Text im Label entsprechend und rufen dann play() auf der SequentialTransition auf.

(b) Todo-Liste 2

Fügen Sie Ihrer Klasse TodoList die Methoden load() und save() hinzu. Beide Methoden bekommen einen String mit dem Dateipfad und speichern bzw. laden die Tasks als CSV. In der GUI haben Sie dafür spezielle Buttons:

Aus der GUI heraus spezifizieren Sie den Dateinamen, z.B. "./todo.csv". Punkt und Schrägstrich benötigt evtl. Ihr Betriebssystem.